package org.finesys.auth.controller;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.TemporalAccessorUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.finesys.common.constants.CacheConstants;
import org.finesys.common.core.module.R;
import org.finesys.common.core.module.ROpt;
import org.finesys.common.core.util.SpringContextHolder;
import org.finesys.common.security.authentication.handler.AuthAuthenticationFailureHandler;
import org.finesys.common.security.core.constant.OAuth2ErrorCodesExpand;
import org.finesys.common.security.core.exception.OAuthClientException;
import org.finesys.common.security.core.util.OAuth2EndpointUtils;
import org.finesys.system.api.dto.ClientDetailsDto;
import org.finesys.system.api.service.ClientService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.security.authentication.event.LogoutSuccessEvent;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.oauth2.core.http.converter.OAuth2AccessTokenResponseHttpMessageConverter;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.security.Principal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@RestController
@RequestMapping
@RequiredArgsConstructor
@Slf4j
public class TokenEndpoint {

    private final OAuth2AuthorizationService oAuth2AuthorizationService;
    @Qualifier(value = "oauthRedisTemplate")
    private final RedisTemplate<String, Object> redisTemplate;

    private final CacheManager cacheManager;

    private final ClientService clientService;
    private final AuthenticationFailureHandler authenticationFailureHandler = new AuthAuthenticationFailureHandler();
    private final HttpMessageConverter<OAuth2AccessTokenResponse> accessTokenHttpResponseConverter = new OAuth2AccessTokenResponseHttpMessageConverter();

    /**
     * 授权登录页面
     */
    @GetMapping("/token/login")
    public ModelAndView login(ModelAndView modelAndView, @RequestParam("error") String error) {
        modelAndView.addObject("error", error);
        modelAndView.setViewName("templates/ftl/login");
        return modelAndView;
    }


    @GetMapping("/oauth2/confirm_access")
    public ModelAndView confirm(Principal principal, ModelAndView modelAndView,
                                @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId,
                                @RequestParam(OAuth2ParameterNames.SCOPE) String scope,
                                @RequestParam(OAuth2ParameterNames.STATE) String state) {
        ClientDetailsDto clientDetails = ROpt.of(clientService.getByClientId(clientId))
                .getData()
                .orElseThrow(() -> new OAuthClientException("clientId 不合法"));
        Set<String> authorizedScopes = StringUtils.commaDelimitedListToSet(clientDetails.getScope());
        modelAndView.addObject("clientId", clientId);
        modelAndView.addObject("state", state);
        modelAndView.addObject("scopeList", authorizedScopes);
        modelAndView.addObject("principalName", principal.getName());
        modelAndView.setViewName("templates/ftl/confirm");
        return modelAndView;
    }

    /**
     * 校验token
     *
     * @param token 令牌
     */
    @SneakyThrows
    @GetMapping("/token/check_token")
    public void checkToken(String token, HttpServletResponse response, HttpServletRequest request) {
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);

        if (StrUtil.isBlank(token)) {
            httpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
            this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                    new InvalidBearerTokenException(OAuth2ErrorCodesExpand.TOKEN_MISSING));
            return;
        }
        OAuth2Authorization authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        // 如果令牌不存在 返回401
        if (authorization == null || authorization.getAccessToken() == null) {
            this.authenticationFailureHandler.onAuthenticationFailure(request, response,
                    new InvalidBearerTokenException(OAuth2ErrorCodesExpand.INVALID_BEARER_TOKEN));
            return;
        }
        Map<String, Object> claims = authorization.getAccessToken().getClaims();
        OAuth2AccessTokenResponse sendAccessTokenResponse = OAuth2EndpointUtils.sendAccessTokenResponse(authorization,
                claims);
        this.accessTokenHttpResponseConverter.write(sendAccessTokenResponse, MediaType.APPLICATION_JSON, httpResponse);
    }

    @DeleteMapping("/token/logout")
    public R<Boolean> logout(HttpServletRequest request, @RequestHeader(value = HttpHeaders.AUTHORIZATION) String authorization) {
        if (!StringUtils.hasText(authorization)) {
            return R.success();
        }
        String token = authorization.replace(OAuth2AccessToken.TokenType.BEARER.getValue(), "").trim();
        log.info("退出登录，token:{}", token);
        return removeToken(token);
    }

    /**
     * 退出登录
     */
    @RequestMapping("/token/remove")
    public R<Boolean> removeToken(@RequestParam("token") String token) {
        OAuth2Authorization authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        if (authorization == null) {
            return R.success();
        }
        OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
        if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
            return R.success();
        }
        //清除用户信息
        Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
        if (cache != null) {
            cache.evictIfPresent(authorization.getPrincipalName());
        }
        // 清空access token
        oAuth2AuthorizationService.remove(authorization);
        //处理自定义退出事件
        SpringContextHolder.publishEvent(new LogoutSuccessEvent(new PreAuthenticatedAuthenticationToken(authorization.getPrincipalName(), authorization.getRegisteredClientId())));
        return R.success();
    }

    /**
     * 分页查询token
     *
     * @param params 分页参数
     * @return
     */
    @PostMapping("/token/page")
    public R<Page<Map<String, Object>>> tokenPage(@RequestBody Map<String, Object> params) {
        redisTemplate.setValueSerializer(RedisSerializer.java());
        String key = "token::access_token::*";
        Set<String> keys = redisTemplate.keys(key);

        Integer current = MapUtil.getInt(params, "current");
        Integer size = MapUtil.getInt(params, "size");

        List<String> pages = keys.stream().skip((current - 1) * size).limit(size).collect(Collectors.toList());
        List<Object> outhList = redisTemplate.opsForValue().multiGet(pages);
        if (outhList == null) {
            return R.failed();
        }
        List<Map<String, Object>> list = outhList.stream().map(
                o -> {
                    OAuth2Authorization authorization = (OAuth2Authorization) o;
                    Map<String, Object> map = new HashMap<>();
                    map.put("id", authorization.getId());
                    map.put("clientId", authorization.getRegisteredClientId());
                    map.put("username", authorization.getPrincipalName());
                    OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
                    map.put("accessToken", accessToken.getToken().getTokenValue());
                    String expiredAt = TemporalAccessorUtil.format(accessToken.getToken().getExpiresAt(),
                            DatePattern.NORM_DATETIME_FORMATTER);
                    map.put("expiresAt", expiredAt);
                    String issuedAt = TemporalAccessorUtil.format(accessToken.getToken().getIssuedAt(),
                            DatePattern.NORM_DATETIME_FORMATTER);
                    map.put("issuedAt", issuedAt);
                    map.put("tokenType", "access_token");
                    return map;
                }
        ).collect(Collectors.toList());
        Page<Map<String, Object>> page = new Page<Map<String, Object>>(current, size);
        page.setRecords(list);
        page.setTotal(keys.size());
        return R.success(page);
    }

    /**
     * 根据token获取用户信息
     */
    @RequestMapping("/token/getUserByToken")
    public R<Map<String, Object>> getUserByToken(@RequestParam("token") String token) {
        OAuth2Authorization authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        if (authorization == null) {
            return R.failed("token 不存在");
        }
        OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
        if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
            return R.failed("token 不存在");
        }
        Map<String, Object> map = new HashMap<>();
        String id = authorization.getId();
        String userName = authorization.getPrincipalName();
        String clientId = authorization.getRegisteredClientId();
        map.put("id", id);
        map.put("userName", userName);
        map.put("clientId", clientId);
        map.put("token", accessToken.getToken().getTokenValue());
        map.put("tokenType", "access_token");

        return R.success(map);
    }

    /**
     * 校验token
     */
    @RequestMapping("/token/validToken")
    public R<Boolean> validToken(@RequestParam("token") String token) {
        OAuth2Authorization authorization = oAuth2AuthorizationService.findByToken(token, OAuth2TokenType.ACCESS_TOKEN);
        if (authorization == null) {
            return R.failed("token 不存在");
        }
        OAuth2Authorization.Token<OAuth2AccessToken> accessToken = authorization.getAccessToken();
        if (accessToken == null || StrUtil.isBlank(accessToken.getToken().getTokenValue())) {
            return R.failed("token 不存在");
        }
        if (accessToken.isExpired()) {
            return R.failed("token 已过期");
        }
        return R.success(Boolean.TRUE);
    }

}
